package jdiff;

import java.util.*;

/**
 * This class contains method to compare two API objects.
 * The differences are stored in an APIDiff object.
 *
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
public class APIComparator {

	/** 
	 * Top-level object representing the differences between two APIs. 
	 * It is this object which is used to generate the report later on.
	 */
	public APIDiff apiDiff;

	/** 
	 * Package-level object representing the differences between two packages. 
	 * This object is also used to determine which file to write documentation
	 * differences into.
	 */
	public PackageDiff pkgDiff;

	/** Default constructor. */
	public APIComparator() {
		apiDiff = new APIDiff();
	}   

	/** For easy local access to the old API object. */
	private static API oldAPI_;
	/** For easy local access to the new API object. */
	private static API newAPI_;

	/** 
	 * Compare two APIs. 
	 */
	public void compareAPIs(API oldAPI, API newAPI) {
		System.out.println("JDiff: comparing the old and new APIs ...");
		oldAPI_ = oldAPI;
		newAPI_ = newAPI;

		double differs = 0.0;

		APIDiff.oldAPIName_ = oldAPI.name_;
		APIDiff.newAPIName_ = newAPI.name_;

		Collections.sort(oldAPI.packages_);
		Collections.sort(newAPI.packages_);

		// Find packages which were removed in the new API
		Iterator<PackageAPI> iter = oldAPI.packages_.iterator();
		while (iter.hasNext()) {
			PackageAPI oldPkg = (PackageAPI)(iter.next());
			// This search is looking for an *exact* match. This is true in
			// all the *API classes.
			int idx = Collections.binarySearch(newAPI.packages_, oldPkg);
			if (idx < 0) {
				// If there an instance of a package with the same name 
				// in both the old and new API, then treat it as changed,
				// rather than removed and added. There will never be more than
				// one instance of a package with the same name in an API.
				int existsNew = newAPI.packages_.indexOf(oldPkg);
				if (existsNew != -1) {
					// Package by the same name exists in both APIs
					// but there has been some or other change.
					differs += 2.0 * comparePackages(oldPkg, (PackageAPI)(newAPI.packages_.get(existsNew)));
				}  else {
					if (trace)
						System.out.println("Package " + oldPkg.name_ + " was removed");
					apiDiff.packagesRemoved.add(oldPkg);
					differs += 1.0;
				}
			} else {
				// The package exists unchanged in name or doc, but may 
				// differ in classes and their members, so it still needs to 
				// be compared.
				differs += 2.0 * comparePackages(oldPkg, (PackageAPI)(newAPI.packages_.get(idx)));
			}
		} // while (iter.hasNext())

		// Find packages which were added or changed in the new API
		iter = newAPI.packages_.iterator();
		while (iter.hasNext()) {
			PackageAPI newPkg = (PackageAPI)(iter.next());
			int idx = Collections.binarySearch(oldAPI.packages_, newPkg);
			if (idx < 0) {
				// See comments above
				int existsOld = oldAPI.packages_.indexOf(newPkg);
				if (existsOld != -1) {
					// Don't mark a package as added or compare it 
					// if it was already marked as changed
				} else {
					if (trace)
						System.out.println("Package " + newPkg.name_ + " was added");
					apiDiff.packagesAdded.add(newPkg);
					differs += 1.0;
				}
			} else {
				// It will already have been compared above.
			}
		} // while (iter.hasNext())

		// Now that the numbers of members removed and added are known
		// we can deduce more information about changes.
		MergeChanges.mergeRemoveAdd(apiDiff);

		// The percent change statistic reported for all elements in each API is  
		// defined recursively as follows:
		// 
		// %age change = 100 * (added + removed + 2*changed)
		//               -----------------------------------
		//               sum of public elements in BOTH APIs
		//
		// The definition ensures that if all classes are removed and all new classes
		// added, the change will be 100%.
		// Evaluation of the visibility of elements has already been done when the 
		// XML was written out.
		// Note that this doesn't count changes in the modifiers of classes and 
		// packages. Other changes in members are counted.
		Long denom = new Long(oldAPI.packages_.size() + newAPI.packages_.size());
		// This should never be zero because an API always has packages?
		if (denom.intValue() == 0) {
			System.out.println("Error: no packages found in the APIs.");
			return;
		}
		if (trace)
			System.out.println("Top level changes: " + differs + "/" + denom.intValue());
		differs = (100.0 * differs)/denom.doubleValue();

		// Some differences such as documentation changes are not tracked in 
		// the difference statistic, so a value of 0.0 does not mean that there
		// were no differences between the APIs.
		apiDiff.pdiff = differs;
		Double percentage = new Double(differs);
		int approxPercentage = percentage.intValue();
		if (approxPercentage == 0)
			System.out.println(" Approximately " + percentage + "% difference between the APIs");
		else
			System.out.println(" Approximately " + approxPercentage + "% difference between the APIs");

		Diff.closeDiffFile();
	}   

	/** 
	 * Compare two packages.
	 */
	public double comparePackages(PackageAPI oldPkg, PackageAPI newPkg) {
		if (trace)
			System.out.println("Comparing old package " + oldPkg.name_ + 
					" and new package " + newPkg.name_);
		pkgDiff = new PackageDiff(oldPkg.name_);
		double differs = 0.0;

		Collections.sort(oldPkg.classes_);
		Collections.sort(newPkg.classes_);

		// Find classes which were removed in the new package
		Iterator<ClassAPI> iter = oldPkg.classes_.iterator();
		while (iter.hasNext()) {
			ClassAPI oldClass = (ClassAPI)(iter.next());
			// This search is looking for an *exact* match. This is true in
			// all the *API classes.
			int idx = Collections.binarySearch(newPkg.getClasses(), oldClass);
			//int idx = Collections.binarySearch(newPkg.classes_, oldClass);
			if (idx < 0) {
				// If there an instance of a class with the same name 
				// in both the old and new package, then treat it as changed,
				// rather than removed and added. There will never be more than
				// one instance of a class with the same name in a package.
				int existsNew = newPkg.classes_.indexOf(oldClass);
				if (existsNew != -1) {
					// Class by the same name exists in both packages
					// but there has been some or other change.
					differs += 2.0 * compareClasses(oldClass, (ClassAPI)(newPkg.classes_.get(existsNew)), pkgDiff);
				}  else {
					if (trace)
						System.out.println("  Class " + oldClass.name_ + " was removed");
					pkgDiff.classesRemoved.add(oldClass);
					differs += 1.0;
				}
			} else {
				// The class exists unchanged in name or modifiers, but may 
				// differ in members, so it still needs to be compared.
				differs += 2.0 * compareClasses(oldClass, (ClassAPI)(newPkg.classes_.get(idx)), pkgDiff);
			}
		} // while (iter.hasNext())

		// Find classes which were added or changed in the new package
		iter = newPkg.classes_.iterator();
		while (iter.hasNext()) {
			ClassAPI newClass = (ClassAPI)(iter.next());
			int idx = Collections.binarySearch(oldPkg.getClasses(), newClass);
			if (idx < 0) {
				// See comments above
				int existsOld = oldPkg.classes_.indexOf(newClass);
				if (existsOld != -1) {
					// Don't mark a class as added or compare it 
					// if it was already marked as changed
				} else {
					if (trace)
						System.out.println("  Class " + newClass.name_ + " was added");
					pkgDiff.classesAdded.add(newClass);
					differs += 1.0;
				}
			} else {
				// It will already have been compared above.
			}
		} // while (iter.hasNext())

		// Check if the only change was in documentation. Bug 472521.
		boolean differsFlag = false;
		if (docChanged(oldPkg.doc_, newPkg.doc_)) {
			String link = "<a href=\"pkg_" + oldPkg.name_ + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
			String id = oldPkg.name_ + "!package";
			String title = link + "Package <b>" + oldPkg.name_ + "</b></a>";
			pkgDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, null, oldPkg.doc_, newPkg.doc_, id, title);
			differsFlag = true;
		}

		// Only add to the parent Diff object if some difference has been found
		if (differs != 0.0 || differsFlag) 
			apiDiff.packagesChanged.add(pkgDiff);

		Long denom = new Long(oldPkg.classes_.size() + newPkg.classes_.size());
		// This should never be zero because a package always has classes?
		if (denom.intValue() == 0) {
			System.out.println("Warning: no classes found in the package " + oldPkg.name_);
			return 0.0;
		}
		if (trace)
			System.out.println("Package " + pkgDiff.name_ + " had a difference of " + differs + "/" + denom.intValue());
		pkgDiff.pdiff = 100.0 * differs/denom.doubleValue();
		return differs/denom.doubleValue();
	} // comparePackages()

	/** 
	 * Compare two classes. 
	 *
	 * Need to compare constructors, methods and fields.
	 */
	public double compareClasses(ClassAPI oldClass, ClassAPI newClass, PackageDiff pkgDiff) {
		if (trace)
			System.out.println("  Comparing old class " + oldClass.name_ + 
					" and new class " + newClass.name_);
		boolean differsFlag = false;
		double differs = 0.0;
		ClassDiff classDiff = new ClassDiff(oldClass.name_);
		classDiff.isInterface_ = newClass.isInterface_; // Used in the report

		// Track changes in modifiers - class or interface
		if (oldClass.isInterface_ != newClass.isInterface_) {
			classDiff.modifiersChange_  = "Changed from ";
			if (oldClass.isInterface_)
				classDiff.modifiersChange_ += "an interface to a class.";
			else
				classDiff.modifiersChange_ += "a class to an interface.";
			differsFlag = true;
		}
		// Track changes in inheritance
		String inheritanceChange = ClassDiff.diff(oldClass, newClass);
		if (inheritanceChange != null) {
			classDiff.inheritanceChange_ = inheritanceChange;
			classDiff._seba_oldClass = oldClass;
			classDiff._seba_newClass = newClass;
			differsFlag = true;
		}
		// Abstract or not
		if (oldClass.isAbstract_ != newClass.isAbstract_) {
			String changeText = "";
			if (oldClass.isAbstract_)
				changeText += "Changed from abstract to non-abstract.";
			else
				changeText += "Changed from non-abstract to abstract.";
			classDiff.addModifiersChange(changeText);
			differsFlag = true;
		}
		// Track changes in documentation
		if (docChanged(oldClass.doc_, newClass.doc_)) {
			String fqName = pkgDiff.name_ + "." + classDiff.name_;
			String link = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
			String id = pkgDiff.name_ + "." + classDiff.name_ + "!class";
			String title = link + "Class <b>" + classDiff.name_ + "</b></a>";
			classDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_,
					classDiff.name_, oldClass.doc_, newClass.doc_, id, title);
			differsFlag = true;
		}
		// All other modifiers
		String modifiersChange = oldClass.modifiers_.diff(newClass.modifiers_);
		if (modifiersChange != null) {
			differsFlag = true;
			if (modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
				System.out.println("JDiff: warning: change from deprecated to undeprecated for class " + pkgDiff.name_ + "." + newClass.name_);

			}
		}
		classDiff.addModifiersChange(modifiersChange);

		// Track changes in members
		boolean differsCtors = 
			compareAllCtors(oldClass, newClass, classDiff);
		boolean differsMethods = 
			compareAllMethods(oldClass, newClass, classDiff);
		boolean differsFields = 
			compareAllFields(oldClass, newClass, classDiff);
		if (differsCtors || differsMethods || differsFields) 
			differsFlag = true;

		if (trace) {
			System.out.println("  Ctors differ? " + differsCtors + 
					", Methods differ? " + differsMethods + 
					", Fields differ? " + differsFields);
		}

		// Only add to the parent if some difference has been found
		if (differsFlag) 
			pkgDiff.classesChanged.add(classDiff);

		// Get the numbers of affected elements from the classDiff object
		differs = 
			classDiff.ctorsRemoved.size() + classDiff.ctorsAdded.size() +
			classDiff.ctorsChanged.size() +
			classDiff.methodsRemoved.size() + classDiff.methodsAdded.size() +
			classDiff.methodsChanged.size() +
			classDiff.fieldsRemoved.size() + classDiff.fieldsAdded.size() +
			classDiff.fieldsChanged.size();
		Long denom = new Long(
				oldClass.ctors_.size() + 
				numLocalMethods(oldClass.methods_) + 
				numLocalFields(oldClass.fields_) +
				newClass.ctors_.size() + 
				numLocalMethods(newClass.methods_) + 
				numLocalFields(newClass.fields_));
		if (denom.intValue() == 0) {
			// This is probably a placeholder interface, but documentation
			// or modifiers etc may have changed
			if (differsFlag) {
				classDiff.pdiff = 0.0; // 100.0 is too much
				return 1.0;
			} else {
				return 0.0;
			}
		}
		// Handle the case where the only change is in documentation or
		// the modifiers
		if (differsFlag && differs == 0.0) {
			differs = 1.0;
		}
		if (trace)
			System.out.println("  Class " + classDiff.name_ + " had a difference of " + differs + "/" + denom.intValue());
		classDiff.pdiff = 100.0 * differs/denom.doubleValue();
		return differs/denom.doubleValue();
	} // compareClasses()

	/** 
	 * Compare all the constructors in two classes. 
	 *
	 * The compareTo method in the ConstructorAPI class acts only upon the type.
	 */
	public boolean compareAllCtors(ClassAPI oldClass, ClassAPI newClass, 
			ClassDiff classDiff) {
		if (trace)
			System.out.println("    Comparing constructors: #old " + 
					oldClass.ctors_.size() + ", #new " + newClass.ctors_.size());
		boolean differs = false;
		boolean singleCtor = false; // Set if there is only one ctor

		Collections.sort(oldClass.ctors_);
		Collections.sort(newClass.ctors_);

		// Find ctors which were removed in the new class
		Iterator<ConstructorAPI> iter = oldClass.ctors_.iterator();
		while (iter.hasNext()) {
			ConstructorAPI oldCtor = (ConstructorAPI)(iter.next());
			int idx = Collections.binarySearch(newClass.ctors_, oldCtor);
			if (idx < 0) {
				int oldSize = oldClass.ctors_.size();
				int newSize = newClass.ctors_.size();
				if (oldSize == 1 && oldSize == newSize) {
					// If there is one constructor in the oldClass and one
					// constructor in the new class, then mark it as changed
					MemberDiff memberDiff = new MemberDiff(oldClass.name_);
					memberDiff.oldType_ = oldCtor.type_;
					memberDiff.oldExceptions_ = oldCtor.exceptions_;
					ConstructorAPI newCtor  = (ConstructorAPI)(newClass.ctors_.get(0));
					memberDiff.newType_ = newCtor.type_;
					memberDiff.newExceptions_ = newCtor.exceptions_;
					// Track changes in documentation
					if (docChanged(oldCtor.doc_, newCtor.doc_)) {
						String type = memberDiff.newType_;
						if (type.compareTo("void") == 0)
							type = "";
						String fqName = pkgDiff.name_ + "." + classDiff.name_;
						String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
						String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + ".ctor_changed(" + type + ")\" class=\"hiddenlink\">";
						String id = pkgDiff.name_ + "." + classDiff.name_ + ".ctor(" + HTMLReportGenerator.simpleName(type) + ")";
						String title = link1 + "Class <b>" + classDiff.name_ + 
						"</b></a>, " + link2 + "constructor <b>" + classDiff.name_ + "(" + HTMLReportGenerator.simpleName(type) + ")</b></a>";
						memberDiff.documentationChange_ = Diff.saveDocDiffs(
								pkgDiff.name_, classDiff.name_, oldCtor.doc_, newCtor.doc_, id, title);
					}
					String modifiersChange = oldCtor.modifiers_.diff(newCtor.modifiers_);
					if (modifiersChange != null && modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
						System.out.println("JDiff: warning: change from deprecated to undeprecated for a constructor in class" + newClass.name_);
					}
					memberDiff.addModifiersChange(modifiersChange);
					if (trace)
						System.out.println("    The single constructor was changed");
					classDiff.ctorsChanged.add(memberDiff);
					singleCtor = true;
				} else {
					if (trace)
						System.out.println("    Constructor " + oldClass.name_ + " was removed");
					classDiff.ctorsRemoved.add(oldCtor);
				}
				differs = true;
			}
		} // while (iter.hasNext())

		// Find ctors which were added in the new class
		iter = newClass.ctors_.iterator();
		while (iter.hasNext()) {
			ConstructorAPI newCtor = (ConstructorAPI)(iter.next());
			int idx = Collections.binarySearch(oldClass.ctors_, newCtor);
			if (idx < 0) {
				if (!singleCtor) {
					if (trace)
						System.out.println("    Constructor " + oldClass.name_ + " was added");
					classDiff.ctorsAdded.add(newCtor);
					differs = true;
				}
			}
		} // while (iter.hasNext())

		return differs;
	} // compareAllCtors()

	/** 
	 * Compare all the methods in two classes. 
	 *
	 * We have to deal with the cases where:
	 *  - there is only one method with a given name, but its signature changes
	 *  - there is more than one method with the same name, and some of them 
	 *    may have signature changes
	 * The simplest way to deal with this is to make the MethodAPI comparator
	 * check the params and return type, as well as the name. This means that
	 * changing a parameter's type would cause the method to be seen as 
	 * removed and added. To avoid this for the simple case, check for before 
	 * recording a method as removed or added.
	 */
	public boolean compareAllMethods(ClassAPI oldClass, ClassAPI newClass, ClassDiff classDiff) {
		if (trace)
			System.out.println("    Comparing methods: #old " + 
					oldClass.methods_.size() + ", #new " +
					newClass.methods_.size());
		boolean differs = false;

		Collections.sort(oldClass.methods_);
		Collections.sort(newClass.methods_);

		// Find methods which were removed in the new class
		Iterator<MethodAPI> iter = oldClass.methods_.iterator();
		while (iter.hasNext()) {
			MethodAPI oldMethod = (MethodAPI)(iter.next());
			int idx = -1;
			MethodAPI[] methodArr = new MethodAPI[newClass.methods_.size()];
			methodArr = (MethodAPI[])newClass.methods_.toArray(methodArr);
			for (int methodIdx = 0; methodIdx < methodArr.length; methodIdx++) {
				MethodAPI newMethod = methodArr[methodIdx];
				if (oldMethod.compareTo(newMethod) == 0) {
					idx  = methodIdx;
					break;
				}
			}
			// NOTE: there was a problem with the binarySearch for 
			// java.lang.Byte.toString(byte b) returning -16 when the compareTo method
			// returned 0 on entry 13. Changed to use arrays instead, so maybe it was
			// an issue with methods having another List of params used indirectly by 
			// compareTo(), unlike constructors and fields?
			//            int idx = Collections.binarySearch(newClass.methods_, oldMethod);
			if (idx < 0) {
				// If there is only one instance of a method with this name 
				// in both the old and new class, then treat it as changed,
				// rather than removed and added.
				// Find how many instances of this method name there are in
				// the old and new class. The equals comparator is just on 
				// the method name.
				int startOld = oldClass.methods_.indexOf(oldMethod); 
				int endOld = oldClass.methods_.lastIndexOf(oldMethod);
				int startNew = newClass.methods_.indexOf(oldMethod); 
				int endNew = newClass.methods_.lastIndexOf(oldMethod);

				if (startOld != -1 && startOld == endOld && 
						startNew != -1 && startNew == endNew) {
					MethodAPI newMethod = (MethodAPI)(newClass.methods_.get(startNew));
					// Only one method with that name exists in both packages,
					// so it is valid to compare the two methods. We know it 
					// has changed, because the binarySearch did not find it.
					if (oldMethod.inheritedFrom_ == null || 
							newMethod.inheritedFrom_ == null) {
						// We also know that at least one of the methods is 
						// locally defined.
						compareMethods(oldMethod, newMethod, classDiff);
						differs = true;
					}
				} else if (oldMethod.inheritedFrom_ == null) {
					// Only concerned with locally defined methods
					if (trace)
						System.out.println("    Method " + oldMethod.name_ + 
								"(" + oldMethod.getSignature() + 
						") was removed");
					classDiff.methodsRemoved.add(oldMethod);
					differs = true;
				}
			}
		} // while (iter.hasNext())

		// Find methods which were added in the new class
		iter = newClass.methods_.iterator();
		while (iter.hasNext()) {
			MethodAPI newMethod = (MethodAPI)(iter.next());
			// Only concerned with locally defined methods
			if (newMethod.inheritedFrom_ != null)
				continue;
			int idx = -1;
			MethodAPI[] methodArr = new MethodAPI[oldClass.methods_.size()];
			methodArr = (MethodAPI[])oldClass.methods_.toArray(methodArr);
			for (int methodIdx = 0; methodIdx < methodArr.length; methodIdx++) {
				MethodAPI oldMethod = methodArr[methodIdx];
				if (newMethod.compareTo(oldMethod) == 0) {
					idx  = methodIdx;
					break;
				}
			}
			// See note above about searching an array instead of binarySearch
			//            int idx = Collections.binarySearch(oldClass.methods_, newMethod);
			if (idx < 0) {
				// See comments above
				int startOld = oldClass.methods_.indexOf(newMethod); 
				int endOld = oldClass.methods_.lastIndexOf(newMethod);
				int startNew = newClass.methods_.indexOf(newMethod); 
				int endNew = newClass.methods_.lastIndexOf(newMethod);

				if (startOld != -1 && startOld == endOld && 
						startNew != -1 && startNew == endNew) {
					// Don't mark a method as added if it was marked as changed
					// The comparison will have been done just above here.
				} else {
					if (trace)
						System.out.println("    Method " + newMethod.name_ + 
								"(" + newMethod.getSignature() + ") was added");
					classDiff.methodsAdded.add(newMethod);
					differs = true;
				}
			}
		} // while (iter.hasNext())

		return differs;
	} // compareAllMethods()

	/** 
	 * Compare two methods which have the same name. 
	 */
	public boolean compareMethods(MethodAPI oldMethod, MethodAPI newMethod, ClassDiff classDiff) {
		MemberDiff methodDiff = new MemberDiff(oldMethod.name_);
		boolean differs = false;
		// Check changes in return type
		methodDiff.oldType_ = oldMethod.returnType_;
		methodDiff.newType_ = newMethod.returnType_;
		if (oldMethod.returnType_.compareTo(newMethod.returnType_) != 0) {
			differs = true;
		}
		// Check changes in signature
		String oldSig = oldMethod.getSignature();
		String newSig = newMethod.getSignature();
		methodDiff.oldSignature_ = oldSig;
		methodDiff.newSignature_ = newSig;
		if (oldSig.compareTo(newSig) != 0) {
			differs = true;
		}
		// Changes in inheritance
		int inh = changedInheritance(oldMethod.inheritedFrom_, newMethod.inheritedFrom_);
		if (inh != 0)
			differs = true;
		if (inh == 1) {
			methodDiff.addModifiersChange("Method was locally defined, but is now inherited from " + linkToClass(newMethod, true) + ".");
			methodDiff.inheritedFrom_ = newMethod.inheritedFrom_;
		} else if (inh == 2) {
			methodDiff.addModifiersChange("Method was inherited from " + linkToClass(oldMethod, false) + ", but is now defined locally.");
		} else if (inh == 3) {
			methodDiff.addModifiersChange("Method was inherited from " + 
					linkToClass(oldMethod, false) + ", and is now inherited from " + linkToClass(newMethod, true) + ".");
			methodDiff.inheritedFrom_ = newMethod.inheritedFrom_;
		}
		// Abstract or not
		if (oldMethod.isAbstract_ != newMethod.isAbstract_) {
			String changeText = "";
			if (oldMethod.isAbstract_)
				changeText += "Changed from abstract to non-abstract.";
			else
				changeText += "Changed from non-abstract to abstract.";
			methodDiff.addModifiersChange(changeText);
			differs = true;
		}
		// Native or not
		if (Diff.showAllChanges && 
				oldMethod.isNative_ != newMethod.isNative_) {
			String changeText = "";
			if (oldMethod.isNative_)
				changeText += "Changed from native to non-native.";
			else
				changeText += "Changed from non-native to native.";
			methodDiff.addModifiersChange(changeText);
			differs = true;
		}
		// Synchronized or not
		if (Diff.showAllChanges && 
				oldMethod.isSynchronized_ != newMethod.isSynchronized_) {
			String changeText = "";
			if (oldMethod.isSynchronized_)
				changeText += "Changed from synchronized to non-synchronized.";
			else
				changeText += "Changed from non-synchronized to synchronized.";
			methodDiff.addModifiersChange(changeText);
			differs = true;
		}

		// Check changes in exceptions thrown
		methodDiff.oldExceptions_ = oldMethod.exceptions_;
		methodDiff.newExceptions_ = newMethod.exceptions_;
		if (oldMethod.exceptions_.compareTo(newMethod.exceptions_) != 0) {
			differs = true;
		}

		// Track changes in documentation
		if (docChanged(oldMethod.doc_, newMethod.doc_)) {
			String sig = methodDiff.newSignature_;
			if (sig.compareTo("void") == 0)
				sig = "";
			String fqName = pkgDiff.name_ + "." + classDiff.name_;
			String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
			String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + newMethod.name_ + "_changed(" + sig + ")\" class=\"hiddenlink\">";
			String id = pkgDiff.name_ + "." + classDiff.name_ + ".dmethod." + newMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")";
			String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
			link2 + HTMLReportGenerator.simpleName(methodDiff.newType_) + " <b>" + newMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")</b></a>";
			methodDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, oldMethod.doc_, newMethod.doc_, id, title);
			differs = true;
		}

		// All other modifiers
		String modifiersChange = oldMethod.modifiers_.diff(newMethod.modifiers_);
		if (modifiersChange != null) {
			differs = true;
			if (modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
				System.out.println("JDiff: warning: change from deprecated to undeprecated for method " +  classDiff.name_ + "." + newMethod.name_);

			}
		}
		methodDiff.addModifiersChange(modifiersChange);

		// Only add to the parent if some difference has been found
		if (differs) {
			if (trace) {
				System.out.println("    Method " + newMethod.name_ + 
						" was changed: old: " + 
						oldMethod.returnType_ + "(" + oldSig + "), new: " +
						newMethod.returnType_ + "(" + newSig + ")");
				if (methodDiff.modifiersChange_ != null)
					System.out.println("    Modifier change: " + methodDiff.modifiersChange_);
			}
			classDiff.methodsChanged.add(methodDiff);
		}

		return differs;
	} // compareMethods()

	/** 
	 * Compare all the fields in two classes. 
	 */
	public boolean compareAllFields(ClassAPI oldClass, ClassAPI newClass, 
			ClassDiff classDiff) {
		if (trace)
			System.out.println("    Comparing fields: #old " + 
					oldClass.fields_.size() + ", #new " 
					+ newClass.fields_.size());
		boolean differs = false;

		Collections.sort(oldClass.fields_);
		Collections.sort(newClass.fields_);

		// Find fields which were removed in the new class
		Iterator<FieldAPI> iter = oldClass.fields_.iterator();
		while (iter.hasNext()) {
			FieldAPI oldField = (FieldAPI)(iter.next());
			int idx = Collections.binarySearch(newClass.fields_, oldField);
			if (idx < 0) {
				// If there an instance of a field with the same name 
				// in both the old and new class, then treat it as changed,
				// rather than removed and added. There will never be more than
				// one instance of a field with the same name in a class.
				int existsNew = newClass.fields_.indexOf(oldField);
				if (existsNew != -1) {
					FieldAPI newField = (FieldAPI)(newClass.fields_.get(existsNew));
					if (oldField.inheritedFrom_ == null || 
							newField.inheritedFrom_ == null) {
						// We also know that one of the fields is locally defined.
						MemberDiff memberDiff = new MemberDiff(oldField.name_);
						memberDiff.oldType_ = oldField.type_;
						memberDiff.newType_ = newField.type_;
						// Changes in inheritance
						int inh = changedInheritance(oldField.inheritedFrom_, newField.inheritedFrom_);
						if (inh != 0)
							differs = true;
						if (inh == 1) {
							memberDiff.addModifiersChange("Field was locally defined, but is now inherited from " + linkToClass(newField, true) + ".");
							memberDiff.inheritedFrom_ = newField.inheritedFrom_;
						} else if (inh == 2) {
							memberDiff.addModifiersChange("Field was inherited from " + linkToClass(oldField, false) + ", but is now defined locally.");
						} else if (inh == 3) {
							memberDiff.addModifiersChange("Field was inherited from " + linkToClass(oldField, false) + ", and is now inherited from " + linkToClass(newField, true) + ".");
							memberDiff.inheritedFrom_ = newField.inheritedFrom_;
						}
						// Transient or not
						if (oldField.isTransient_ != newField.isTransient_) {
							String changeText = "";
							if (oldField.isTransient_)
								changeText += "Changed from transient to non-transient.";
							else
								changeText += "Changed from non-transient to transient.";
							memberDiff.addModifiersChange(changeText);
							differs = true;
						}
						// Volatile or not
						if (oldField.isVolatile_ != newField.isVolatile_) {
							String changeText = "";
							if (oldField.isVolatile_)
								changeText += "Changed from volatile to non-volatile.";
							else
								changeText += "Changed from non-volatile to volatile.";
							memberDiff.addModifiersChange(changeText);
							differs = true;
						}
						// Change in value of the field
						if (oldField.value_ != null &&
								newField.value_ != null &&
								oldField.value_.compareTo(newField.value_) != 0) {
							String changeText = "Changed in value from " + oldField.value_
							+ " to " + newField.value_ +".";
							memberDiff.addModifiersChange(changeText);
							differs = true;
						}
						// Track changes in documentation
						if (docChanged(oldField.doc_, newField.doc_)) {
							String fqName = pkgDiff.name_ + "." + classDiff.name_;
							String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
							String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + newField.name_ + "\" class=\"hiddenlink\">";
							String id = pkgDiff.name_ + "." + classDiff.name_ + ".field." + newField.name_;
							String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
							link2 + HTMLReportGenerator.simpleName(memberDiff.newType_) + " <b>" + newField.name_ + "</b></a>";
							memberDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, oldField.doc_, newField.doc_, id, title);
							differs = true;
						}

						// Other differences
						String modifiersChange = oldField.modifiers_.diff(newField.modifiers_);
						memberDiff.addModifiersChange(modifiersChange);
						if (modifiersChange != null && modifiersChange.indexOf("Change from deprecated to undeprecated") != -1) {
							System.out.println("JDiff: warning: change from deprecated to undeprecated for class " + newClass.name_ + ", field " + newField.name_);
						}
						if (trace)
							System.out.println("    Field " + newField.name_ + " was changed");
						classDiff.fieldsChanged.add(memberDiff);
						differs = true;
					}
				} else if (oldField.inheritedFrom_ == null) {
					if (trace)
						System.out.println("    Field " + oldField.name_ + " was removed");
					classDiff.fieldsRemoved.add(oldField);
					differs = true;
				}
			}
		} // while (iter.hasNext())

		// Find fields which were added in the new class
		iter = newClass.fields_.iterator();
		while (iter.hasNext()) {
			FieldAPI newField = (FieldAPI)(iter.next());
			// Only concerned with locally defined fields
			if (newField.inheritedFrom_ != null)
				continue;
			int idx = Collections.binarySearch(oldClass.fields_, newField);
			if (idx < 0) {
				// See comments above
				int existsOld = oldClass.fields_.indexOf(newField);
				if (existsOld != -1) {
					// Don't mark a field as added if it was marked as changed
				} else {
					if (trace)
						System.out.println("    Field " + newField.name_ + " was added");
					classDiff.fieldsAdded.add(newField);
					differs = true;
				}
			}
		} // while (iter.hasNext())

		return differs;
	} // compareFields()

	/** 
	 * Decide if two blocks of documentation changed. 
	 *
	 * @return true if both are non-null and differ, 
	 *              or if one is null and the other is not.
	 */
	public static boolean docChanged(String oldDoc, String newDoc) {
		if (!HTMLReportGenerator.reportDocChanges)
			return false; // Don't even count doc changes as changes
		if (oldDoc == null && newDoc != null)
			return true;
		if (oldDoc != null && newDoc == null)
			return true;
		if (oldDoc != null && newDoc != null && oldDoc.compareTo(newDoc) != 0)
			return true;
		return false;
	}

	/** 
	 * Decide if two elements changed where they were defined. 
	 *
	 * @return 0 if both are null, or both are non-null and are the same.
	 *         1 if the oldInherit was null and newInherit is non-null.
	 *         2 if the oldInherit was non-null and newInherit is null.
	 *         3 if the oldInherit was non-null and newInherit is non-null 
	 *           and they differ.
	 */
	public static int changedInheritance(String oldInherit, String newInherit) {
		if (oldInherit == null && newInherit == null)
			return 0;
		if (oldInherit == null && newInherit != null)
			return 1;
		if (oldInherit != null && newInherit == null)
			return 2;
		if (oldInherit.compareTo(newInherit) == 0)
			return 0;
		else
			return 3;
	}

	/** 
	 * Generate a link to the Javadoc page for the given method.
	 */
	public static String linkToClass(MethodAPI m, boolean useNew) {
		String sig = m.getSignature();
		if (sig.compareTo("void") == 0)
			sig = "";
		return linkToClass(m.inheritedFrom_, m.name_, sig, useNew);
	}

	/** 
	 * Generate a link to the Javadoc page for the given field.
	 */
	public static String linkToClass(FieldAPI m, boolean useNew) {
		return linkToClass(m.inheritedFrom_, m.name_, null, useNew);
	}

	/** 
	 * Given the name of the class, generate a link to a relevant page.
	 * This was originally for inheritance changes, so the JDiff page could 
	 * be a class changes page, or a section in a removed or added classes 
	 * table. Since there was no easy way to tell which type the link
	 * should be, it is now just a link to the relevant Javadoc page.
	 */
	public static String linkToClass(String className, String memberName, 
			String memberType, boolean useNew) {
		if (!useNew && HTMLReportGenerator.oldDocPrefix == null) {
			return "<tt>" + className + "</tt>"; // No link possible
		}
		API api = oldAPI_;
		String prefix = HTMLReportGenerator.oldDocPrefix;
		if (useNew) {
			api = newAPI_;
			prefix = HTMLReportGenerator.newDocPrefix;
		}
		ClassAPI cls = (ClassAPI)api.classes_.get(className);
		if (cls == null) {
			if (useNew)
				System.out.println("Warning: class " + className + " not found in the new API when creating Javadoc link");
			else
				System.out.println("Warning: class " + className + " not found in the old API when creating Javadoc link");
			return "<tt>" + className + "</tt>";
		}
		int clsIdx = className.indexOf(cls.name_);
		if (clsIdx != -1) {
			String pkgRef = className.substring(0, clsIdx);
			pkgRef = pkgRef.replace('.', '/');
			String res = "<a href=\"" + prefix + pkgRef + cls.name_ + ".html#" + memberName;
			if (memberType != null)
				res += "(" + memberType + ")";
			res += "\" target=\"_top\">" + "<tt>" + cls.name_ + "</tt></a>";
			return res;
		}
		return "<tt>" + className + "</tt>";
	}    

	/** 
	 * Return the number of methods which are locally defined.
	 */
	public int numLocalMethods(List<MethodAPI> methods) {
		int res = 0;
		Iterator<MethodAPI> iter = methods.iterator();
		while (iter.hasNext()) {
			MethodAPI m = (MethodAPI)(iter.next());
			if (m.inheritedFrom_ == null) 
				res++;
		}
		return res;
	}

	/** 
	 * Return the number of fields which are locally defined.
	 */
	public int numLocalFields(List<FieldAPI> fields) {
		int res = 0;
		Iterator<FieldAPI> iter = fields.iterator();
		while (iter.hasNext()) {
			FieldAPI f = (FieldAPI)(iter.next());
			if (f.inheritedFrom_ == null) 
				res++;
		}
		return res;
	}

	/** Set to enable increased logging verbosity for debugging. */
	private boolean trace = false;
}
